package vknob;

/**
 * An immutable implementation of VKnobSnapper.
 */
public class DefaultVKnobSnapper
       implements VKnobSnapper {

    private final int numOfBlocks;
    private final float leftSlopeOffset, flatAreaOffset,
            rightSlopeOffset, rightSnapOffset;
    private final float slopeK; // the angel of the slopes as given by y = k*x + m

    /* A snap block:
     * start snap, right slope of start snap, flat area,
     * left slope of end snap, end snap.
     *
     *  |  ________  |
     *  |_/        \_|
     *
     */
     
    /**
     * Constructs a DefaultVKnobSnapper with 10 snaps.
     * @see #DefaultVKnobSnapper(int,float,float)
     */ 
    public DefaultVKnobSnapper() {
        this(10);
    }
    /**
     * Constructs a DefaultVKnobSnapper with 0.3 as the snap width.
     * @see #DefaultVKnobSnapper(int,float,float)
     */ 
    public DefaultVKnobSnapper(int numOfSnaps) {
        this(numOfSnaps, 0.3f);
    }
    /**
     * Constructs a DefaultVKnobSnapper with 0.5 as the snap softness.
     * @see #DefaultVKnobSnapper(int,float,float)
     */ 
    public DefaultVKnobSnapper(int numOfSnaps, float width) {
        this(numOfSnaps, width, 0.5f);
    }

    /**
     * Constructs a DefaultVKnobSnapper with the given values.
     *
     * To allow only the values on the snaps, set width to 1.
     * To disable snapping, set width to 0.
     *
     * @param numOfSnaps The number of snaps; 0 or greater.
     * @param width A number between 0 and 1.
     * @param softness A number between 0 and 1.
     */
    public DefaultVKnobSnapper(int numOfSnaps, float width, float softness) {
        this.numOfBlocks = numOfSnaps;
        float blockWidth = 1f / numOfBlocks;
        leftSlopeOffset   = width * blockWidth / 2;
        flatAreaOffset = leftSlopeOffset
                + softness * (blockWidth / 2 - leftSlopeOffset);
        rightSlopeOffset = blockWidth - flatAreaOffset;
        rightSnapOffset   = blockWidth - leftSlopeOffset;
        slopeK = flatAreaOffset / (flatAreaOffset - leftSlopeOffset);
    }

    public float[] getSnapPositions() {
        float[] snaps = new float[numOfBlocks + 1];
        for (int block = 0; block <= numOfBlocks; block++)
                snaps[block] = ((float)block) / numOfBlocks;
        return snaps;
    }

    public float getKnobDirection(float x) {
        int block = (int)(x * numOfBlocks);
        float blockStart = ((float)block) / numOfBlocks;
        float leftSlope, flatArea,  // start positions of block parts
             rightSlope, rightSnap;
        
        return (x <= (leftSlope = blockStart + leftSlopeOffset))
               ? // Within the start snap
                   blockStart
               : (x <= (flatArea = blockStart + flatAreaOffset))
               ?
                   // On the right slope of the start snap
                   slopeK * x + (1 - slopeK) * flatArea
               : (x <= (rightSlope = blockStart + rightSlopeOffset))
               ?
                   // On the flat land between the snaps
                   x
               : (x <= (rightSnap = blockStart + rightSnapOffset))
               ?   
                   // On the left slope of the end snap
                   slopeK * x + (1 - slopeK) * rightSlope
               :   
                   // in the end snap
                   ((float)block + 1) / numOfBlocks;
    }
    
    /** The inverse of getKnobDirection(float x) */
    public float getDragValue(float y) {
        int block = (int)(y * numOfBlocks);
        float blockStart, blockEnd, // start positions of block parts
              flatArea, rightSlope;
             
        return (y == (blockStart = ((float)block) / numOfBlocks))
               ? y // the start snap
               : (y <= (flatArea = blockStart + flatAreaOffset))
               ?
                   // the left slope
                   (y - (1 - slopeK) * flatArea) / slopeK
               : (y <= (rightSlope = blockStart + rightSlopeOffset))
               ? y // On the flat land between the snaps
               : (y < (blockEnd = ((float)block + 1)) / numOfBlocks)
               ? // the right slope
                   (y - (1 - slopeK) * rightSlope) / slopeK
               : y; // the end snap
    }
}
